package org.infodavid.common.test.model;

import java.lang.ref.WeakReference;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.rmi.NoSuchObjectException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

/**
 * The Class GeneratorUtil.
 */
@SuppressWarnings("rawtypes")
public final class GeneratorUtil {

  /** The Constant log. */
  @SuppressWarnings("unused")
  private static final Log LOGGER = LogFactory.getLog(GeneratorUtil.class);// NOPMD

  /** The singleton. */
  private static WeakReference<GeneratorUtil> singleton = null;

  /** The Constant DEFAULT_VALUES. */
  public static final Map<Class,Object> DEFAULT_VALUES;

  static {
    DEFAULT_VALUES = new HashMap<Class,Object>();
  }

  /**
   * returns the singleton.
   * @return the singleton
   */
  public static GeneratorUtil getInstance() {
    if (singleton == null || singleton.get() == null) {
      singleton = new WeakReference<GeneratorUtil>(new GeneratorUtil());
    }

    return singleton.get();
  }

  /**
   * Instantiates a new reflect util.
   */
  private GeneratorUtil() {
    super();
  }

  /**
   * Fill all fields using random data.
   * @param obj the object to use
   * @param modifiers the modifiers
   */
  public static void fillFields(final Object obj, final int... modifiers) {
    Class type = obj.getClass();

    while (type != null) {
      final Field[] fields = type.getDeclaredFields();

      if (fields != null) {
        int i = 0;
        final int count = fields.length;
        Field field;
        Object value;

        Arrays.sort(modifiers);

        while (i < count) {
          field = fields[i];

          if (Arrays.binarySearch(modifiers, fields[i].getModifiers()) >= 0) {
            field.setAccessible(true);

            try {
              value = getValue(field.getType());
              field.set(obj, value);
            }
            catch (final Exception e) {
              e.printStackTrace();
            }
          }

          i++;
        }
      }

      if (type.getSuperclass() == type) {
        type = null;
      }
      else {
        type = type.getSuperclass();
      }
    }
  }

  /**
   * Gets the generic type.
   * @param actualClass the actual class
   * @param field the field
   * @return the generic type
   */
  public static Class getGenericType(final Class actualClass, final Field field) {
    final Type generic = field.getGenericType();
    Class result = field.getType();

    if (generic instanceof TypeVariable) {
      final Class[] results = getGenericTypes(actualClass, ((TypeVariable)generic).getName());

      if (!(results == null || results.length == 0)) {
        result = results[0];
      }
    }

    return result;
  }

  /**
   * Gets the generic types.
   * @param cls the cls
   * @param names the names
   * @return the generic types
   */
  public static Class[] getGenericTypes(final Class cls, final String... names) {
    final TypeVariable[] variables = cls.getTypeParameters();
    Class[] results = null;

    if (variables == null || variables.length == 0) {
      final Type generic = cls.getGenericSuperclass();

      if (generic instanceof ParameterizedType) {
        results = getGenericTypes((ParameterizedType)generic, names);
      }
    }

    return results;
  }

  /**
   * Gets the generic types.
   * @param parameterizedType the parameterized type
   * @param names the names
   * @return the generic types
   */
  public static Class[] getGenericTypes(final ParameterizedType parameterizedType,
      final String... names) {
    final Type[] types = parameterizedType.getActualTypeArguments();
    final Type rawType = parameterizedType.getRawType();
    Class[] results = null;

    if (!(types == null || types.length == 0) && rawType instanceof Class) {
      final Class rawClass = (Class)rawType;
      final List<Class> classes = new ArrayList<Class>();
      final TypeVariable[] variables = rawClass.getTypeParameters();

      if (names == null || names.length == 0 || variables == null || variables.length == 0) {
        for (final Type type : types) {
          if (type instanceof Class) {
            classes.add((Class)type);
          }
        }
      }
      else {
        final String[] namesToReturn = names.clone();
        final int count = variables.length;

        Arrays.sort(namesToReturn);

        for (int i = 0; i < count; i++) {
          if (Arrays.binarySearch(namesToReturn, variables[i].getName()) >= 0
              && types[i] instanceof Class) {
            classes.add((Class)types[i]);
          }
        }
      }

      if (!classes.isEmpty()) {
        results = classes.toArray(new Class[classes.size()]);
      }
    }

    return results;
  }

  /**
   * Gets the value.
   * @param type the type
   * @return the value
   * @throws NoSuchObjectException the no such object exception
   */
  public static Object getValue(final Class type) throws NoSuchObjectException {
    Object value;

    synchronized (DEFAULT_VALUES) {
      value = DEFAULT_VALUES.get(type);
    }

    if (value == null) {
      final Random rand = new Random();

      if (type.isArray()) {
        final Object array = Array.newInstance(type.getComponentType(), 10);
        final int count = Array.getLength(array);

        for (int i = 0; i < count; i++) {
          Array.set(array, i, getValue(type.getComponentType()));
        }

        value = array;
      }
      else if (type == String.class) {
        value = String.valueOf(rand.nextInt());
      }
      else if (type == int.class) {
        value = Integer.valueOf(rand.nextInt());
      }
      else if (type == long.class) {
        value = Long.valueOf(rand.nextLong());
      }
      else if (type == short.class) {
        value = Short.valueOf((short)rand.nextInt());
      }
      else if (type == float.class) {
        value = Float.valueOf(rand.nextFloat());
      }
      else if (type == double.class) {
        value = Double.valueOf(rand.nextDouble());
      }
      else if (type == BigDecimal.class) {
        value = BigDecimal.valueOf(rand.nextDouble());
      }
      else if (type == boolean.class) {
        value = Boolean.valueOf(rand.nextBoolean());
      }
      else if (type == BigInteger.class) {
        value = BigInteger.valueOf(rand.nextInt());
      }
      else if (type == Double.class) {
        value = Double.valueOf(rand.nextDouble());
      }
      else if (type == Boolean.class) {
        value = Boolean.valueOf(rand.nextBoolean());
      }
      else if (type == Long.class) {
        value = Long.valueOf(rand.nextLong());
      }
      else if (type == Integer.class) {
        value = Integer.valueOf(rand.nextInt());
      }
      else if (type == Short.class) {
        value = Short.valueOf((short)rand.nextInt());
      }
      else if (type == Float.class) {
        value = Float.valueOf(rand.nextFloat());
      }
      else if (type == Date.class) {
        value = new Date(rand.nextInt());
      }
      else if (type.isEnum()) {
        final Object[] constants = type.getEnumConstants();
        value = constants[Math.abs(rand.nextInt() % constants.length)];
      }
      else {
        try {
          value = type.newInstance();
        }
        catch (final Throwable e) {
          throw new NoSuchObjectException("No default value for the type: " + type
              + ", add it in the class: " + GeneratorUtil.class.getName());
        }
      }
    }

    return value;
  }

  /**
   * Gets the a random value according to the type of the field.
   * @param obj the obj
   * @param field the field
   * @return the value
   * @throws NoSuchObjectException the no such object exception
   */
  public static Object getValue(final Object obj, final Field field) throws NoSuchObjectException {
    Object value = null;
    try {
      if (field.getGenericType() == null) {
        value = getValue(field.getType());
      }
      else {
        value = getValue(getGenericType(obj.getClass(), field));
      }
    }
    catch (final NoSuchObjectException e) {
      throw e;
    }
    catch (final Exception e) {
      e.printStackTrace();
      value = getValue(getGenericType(obj.getClass(), field));
    }

    return value;
  }

  /**
   * Value of.
   * @param text the text
   * @param type the type
   * @return the object
   * @throws NoSuchObjectException the no such object exception
   */
  @SuppressWarnings({
      "deprecation", "boxing"
  })
  public static Object valueOf(final String text, final Class type) throws NoSuchObjectException {
    Object value = null;

    if (type.isArray()) {
      final String[] items = text.split(";");
      final Object array = Array.newInstance(type.getComponentType(), items.length);
      int i = 0;

      for (final String item : items) {
        Array.set(array, i, valueOf(item, type.getComponentType()));
        i++;
      }

      value = array;
    }
    else if (type == String.class) {
      value = text;
    }
    else if (type == int.class) {
      value = Integer.valueOf(text);
    }
    else if (type == long.class) {
      value = Long.valueOf(text);
    }
    else if (type == short.class) {
      value = Short.valueOf(text);
    }
    else if (type == float.class) {
      value = Float.valueOf(text);
    }
    else if (type == double.class) {
      value = Double.valueOf(text);
    }
    else if (type == BigDecimal.class) {
      value = new BigDecimal(text);
    }
    else if (type == boolean.class) {
      value = Boolean.valueOf(text);
    }
    else if (type == BigInteger.class) {
      value = new BigInteger(text);
    }
    else if (type == Double.class) {
      value = Double.valueOf(text);
    }
    else if (type == Boolean.class) {
      value = Boolean.valueOf(text);
    }
    else if (type == Long.class) {
      value = Long.valueOf(text);
    }
    else if (type == Integer.class) {
      value = Integer.valueOf(text);
    }
    else if (type == Short.class) {
      value = Short.valueOf(text);
    }
    else if (type == Float.class) {
      value = Float.valueOf(text);
    }
    else if (type == Date.class) {
      value = Date.parse(text);
    }
    else if (type.isEnum()) {
      final Object[] constants = type.getEnumConstants();
      Object constant;
      int i = 0;

      while (value == null && i < constants.length) {
        constant = constants[i++];

        if (constant.toString().equals(text)) {
          value = constant;
        }
      }
    }
    else {
      throw new NoSuchObjectException("No value for the type: " + type + " and text: " + text);
    }

    return value;
  }
}
